/*
 * Package il.ac.biu.cs.grossmm.impl.presence
 * File SqlRegistrar.java
 * 
 * This is a part of presence service framework prototype. 
 * See javadoc for more information.
 *  
 * Copyright (C) 2006 Grossmann Mark
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package il.ac.biu.cs.grossmm.impl.presence;

import il.ac.biu.cs.grossmm.api.OperationFailedException;
import il.ac.biu.cs.grossmm.api.presence.Entity;
import il.ac.biu.cs.grossmm.api.presence.EntityType;
import il.ac.biu.cs.grossmm.api.presence.Registrar;
import il.ac.biu.cs.grossmm.api.server.Component;
import il.ac.biu.cs.grossmm.api.server.ComponentManager;
import il.ac.biu.cs.grossmm.api.sql.SqlDataSource;

import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collections;
import java.util.HashSet;
import java.util.Random;
import java.util.WeakHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.log4j.Logger;

public class SqlRegistrar implements Registrar, Component {
	private static Logger logger = Logger.getLogger(SqlRegistrar.class);

	private SqlDataSource dataSource;

	private Connection conn;

	WeakHashMap<Integer, PermanentEntity> entities = new WeakHashMap<Integer, PermanentEntity>();

	Lock writeLock = new ReentrantLock();

	Lock entitiesLock = new ReentrantLock();

	public void registerRole(Entity entity, EntityType entityType)
			throws OperationFailedException {
		PermanentEntity pe = (PermanentEntity) entity;
		Integer id = pe.id;
		int role_id = entityType.getSerialId();

		Statement s = null;
		writeLock.lock();

		try {
			s = conn.createStatement();
			ResultSet rs = s
					.executeQuery("SELECT id FROM entity_roles WHERE id=" + id
							+ " AND role_id=" + role_id);

			if (rs.next())
				return; // already associated

			update("INSERT INTO entity_roles VALUES (" + id + ", " + role_id
					+ ")");
			conn.commit();

			pe.roleIds.add(role_id);

		} catch (SQLException e) {
			throw new OperationFailedException(e);
		} finally {
			writeLock.unlock();
			try {
				s.close();
			} catch (SQLException e) {
			}
		}
	}

	public boolean deregisterRole(Entity entity, EntityType entityType)
			throws OperationFailedException {
		Statement s = null;
		writeLock.lock();

		PermanentEntity pe = (PermanentEntity) entity;
		Integer id = pe.id;

		boolean wasDeleted;

		try {
			update("DELETE FROM entity_roles WHERE id=" + id + " AND role_id="
					+ entityType.getSerialId());

			s = conn.createStatement();
			ResultSet rs = s
					.executeQuery("SELECT id FROM entity_roles WHERE id=" + id);

			if (!rs.next()) {
				delete(id);
				wasDeleted = true;
			} else
				wasDeleted = false;

			update("CHECKPOINT");
			conn.commit();

			if (!wasDeleted)
				update(pe);

		} catch (SQLException e) {
			throw new OperationFailedException(e);
		} finally {
			writeLock.unlock();
			try {
				if (s != null)
					s.close();
			} catch (SQLException e) {
			}
		}

		return wasDeleted;
	}

	public void deregister(Entity entity) throws OperationFailedException {
		PermanentEntity pe = (PermanentEntity) entity;
		Integer id = pe.id;

		delete(id);
	}

	/**
	 * @param id
	 * @throws OperationFailedException
	 */
	@SuppressWarnings("unchecked")
	private void delete(Integer id) throws OperationFailedException {
		writeLock.lock();

		try {
			updateNoEx("DELETE FROM entity_names WHERE id=" + id);
			updateNoEx("DELETE FROM entity_roles WHERE id=" + id);
			updateNoEx("DELETE FROM entity_props WHERE id=" + id);

			update("CHECKPOINT");
			conn.commit();

		} catch (SQLException e) {
			throw new OperationFailedException(e);
		} finally {
			writeLock.unlock();
		}

		entitiesLock.lock();
		PermanentEntity pe = entities.get(id);
		entitiesLock.unlock();

		if (pe != null) {
			pe.isValid = false;
			pe.identifiers = Collections.EMPTY_SET;
			pe.roleIds = Collections.EMPTY_SET;
		}

		entities.remove(id);
	}

	public boolean deregisterAlias(String identifier)
			throws OperationFailedException {
		Statement s = null;
		writeLock.lock();

		boolean wasDeleted;

		try {
			Integer id = getId(identifier);
			if (id == null)
				return false;

			update("DELETE FROM entity_names WHERE id=" + id + " AND name='"
					+ identifier + "'");

			s = conn.createStatement();
			ResultSet rs = s
					.executeQuery("SELECT id FROM entity_names WHERE id=" + id);

			if (!rs.next()) {
				delete(id);
				wasDeleted = true;
			} else
				wasDeleted = false;

			update("CHECKPOINT");
			conn.commit();

			entitiesLock.lock();
			PermanentEntity pe = entities.get(id);
			entitiesLock.unlock();

			if (pe != null && !wasDeleted)
				update(pe);

		} catch (SQLException e) {
			throw new OperationFailedException(e);
		} finally {
			writeLock.unlock();
			try {
				if (s != null)
					s.close();
			} catch (SQLException e) {
			}
		}

		return wasDeleted;
	}

	public Entity getEntity(String identifier) throws OperationFailedException {
		Integer id;
		PermanentEntity pe;

		try {
			id = getId(identifier);
		} catch (SQLException e) {
			throw new OperationFailedException(e);
		}

		if (id == null)
			return null;

		entitiesLock.lock();
		try {
			pe = entities.get(id);

			if (pe != null)
				return pe;

			pe = new PermanentEntity(this, id);

			update(pe);

			entities.put(id, pe);

			if (!pe.isValid)
				throw new OperationFailedException("Database access error");

			entities.put(id, pe);
		} finally {
			entitiesLock.unlock();
		}

		return pe;
	}

	@SuppressWarnings("unchecked")
	public <T> T getProperty(Entity entity, String property)
			throws OperationFailedException {
		Statement s = null;
		PermanentEntity pe = (PermanentEntity) entity;

		try {
			s = conn.createStatement();

			ResultSet rs = s
					.executeQuery("SELECT value FROM entity_props WHERE id="
							+ pe.id + " AND property='" + property + "'");

			if (!rs.next())
				return null;

			Blob blob = rs.getBlob(1);

			ObjectInputStream ois = new ObjectInputStream(blob
					.getBinaryStream());
			Object value = ois.readObject();
			ois.close();

			return (T) value;
		} catch (Exception e) {
			throw new OperationFailedException(e);
		} finally {
			try {
				s.close();
			} catch (SQLException e) {
			}
		}
	}

	public void registerAlias(Entity entity, String identifier)
			throws OperationFailedException {
		PermanentEntity pe = (PermanentEntity) entity;
		Integer id = pe.id;

		Statement s = null;
		writeLock.lock();

		try {
			s = conn.createStatement();
			ResultSet rs = s
					.executeQuery("SELECT id FROM entity_names WHERE id=" + id
							+ " AND name='" + identifier + "'");

			if (rs.next())
				return; // already associated

			update("INSERT INTO entity_names VALUES (" + id + ", '"
					+ identifier + "')");
			conn.commit();

			pe.identifiers.add(identifier);

		} catch (SQLException e) {
			throw new OperationFailedException(e);
		} finally {
			writeLock.unlock();
			try {
				s.close();
			} catch (SQLException e) {
			}
		}
	}

	public Entity registerEntity(String identifier, EntityType entityType)
			throws OperationFailedException {
		Statement s = null;
		writeLock.lock();

		PermanentEntity pe;

		try {
			Integer id = getId(identifier);
			if (id != null)
				throw new OperationFailedException("Identifier '" + identifier
						+ "' is in use");

			s = conn.createStatement();
			Random r = new Random();
			int count = 1000;
			boolean invalidId;

			do {
				if (0 > --count)
					throw new OperationFailedException(
							"Database is probably full");

				id = r.nextInt();
				ResultSet rs = s
						.executeQuery("SELECT id FROM entity_names WHERE id="
								+ id);
				invalidId = rs.next();
			} while (invalidId);

			update("INSERT INTO entity_names VALUES (" + id + ", '"
					+ identifier + "')");
			update("INSERT INTO entity_roles VALUES (" + id + ", "
					+ entityType.getSerialId() + ")");

			pe = new PermanentEntity(this, id);

			update(pe);

			entitiesLock.lock();
			entities.put(id, pe);
			entitiesLock.unlock();

			update("CHECKPOINT");
			conn.commit();

		} catch (SQLException e) {
			throw new OperationFailedException(e);
		} finally {
			writeLock.unlock();
			if (s != null)
				try {
					s.close();
				} catch (SQLException e) {
				}
		}

		return pe;
	}

	public void removeProperty(Entity entity, String property)
			throws OperationFailedException {
		PermanentEntity pe = (PermanentEntity) entity;

		writeLock.lock();

		try {
			update("DELETE FROM entity_props WHERE id=" + pe.id
					+ " AND property='" + property + "'");
			update(pe);

			update("CHECKPOINT");
			conn.commit();
		} catch (Exception e) {
			throw new OperationFailedException(e);
		} finally {
			writeLock.unlock();
		}
	}

	public void setProperty(Entity entity, String property, Object value)
			throws OperationFailedException {
		PreparedStatement ps = null;
		PermanentEntity pe = (PermanentEntity) entity;
		Integer id = pe.id;

		writeLock.lock();

		try {

			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			ObjectOutputStream oos = new ObjectOutputStream(baos);
			oos.writeObject(value);
			oos.close();

			byte[] bytes = baos.toByteArray();

			update("DELETE FROM entity_props WHERE id=" + id
					+ " AND property='" + property + "'");

			ps = conn
					.prepareStatement("INSERT INTO entity_props VALUES (?, ?, ?)");
			ps.setInt(1, id);
			ps.setString(2, property);
			ps.setBytes(3, bytes);

			ps.execute();

			update("CHECKPOINT");
			conn.commit();

		} catch (Exception e) {
			throw new OperationFailedException(e);
		} finally {
			if (ps != null)
				try {
					ps.close();
				} catch (SQLException e) {
				}
		}
	}

	public void aborted(Object hint, Exception e) throws Exception {
		throw e;
	}

	public void setComponentManager(ComponentManager container)
			throws Exception {
		dataSource = (SqlDataSource) container
				.getComponent(SqlDataSource.class);

		init();
	}

	/**
	 * Resets the registrar to the initial state. Used for testing purposes only
	 */
	void reset() {
		try {
			update("CHECKPOINT");
			conn.commit();
			conn.close();

			entities.clear();

			init();
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private void init() throws SQLException {
		conn = dataSource.getConnection();
		conn.setAutoCommit(false);

		updateNoEx("CREATE TABLE entity_names (id INTEGER, name VARCHAR(256) PRIMARY KEY)");
		updateNoEx("CREATE TABLE entity_roles (id INTEGER PRIMARY KEY, role_id INTEGER)");
		updateNoEx("CREATE TABLE entity_props (id INTEGER PRIMARY KEY, property VARCHAR(256), value VARBINARY)");

		updateNoEx("CREATE INDEX idx_names on entity_names (id)");
		updateNoEx("CREATE INDEX idx_roles on entity_roles (id)");
		updateNoEx("CREATE INDEX idx_props on entity_props (id)");
	}

	protected Integer getId(String name) throws SQLException {
		Statement s = conn.createStatement();
		try {
			ResultSet rs = s
					.executeQuery("SELECT id FROM entity_names WHERE name='"
							+ name + "'");
			if (!rs.next())
				return null;

			return rs.getInt(1);
		} finally {
			s.close();
		}

	}

	/**
	 * Executes sql update command
	 * 
	 * @param sql
	 *            sql update command to execute
	 * @throws SQLException
	 */
	protected void update(String sql) throws SQLException {
		// if (logger.isDebugEnabled()) {
		// logger.debug("UPDATE SQL = [" + sql + "]");
		// }

		Statement st = null;

		st = conn.createStatement(); // statements

		int i = st.executeUpdate(sql); // run the query

		if (i == -1) {
			throw new SQLException("Unknown Error (executeUpadte returned -1)");
		}

		st.close();
	}

	/**
	 * Executes sql update command, ignoring any exceptions
	 * 
	 * @param sql
	 *            sql update command to execute
	 * @throws SQLException
	 */
	protected void updateNoEx(String sql) {
		// if (logger.isDebugEnabled()) {
		// logger.debug("UPDATE SQL = [" + sql + "]");
		// }

		Statement st = null;

		try {
			st = conn.createStatement(); // statements

			int i = st.executeUpdate(sql); // run the query

			if (i == -1) {
				throw new SQLException(
						"Unknown Error (executeUpadte returned -1)");
			}
		} catch (SQLException ignored) {
			if (logger.isDebugEnabled()) {
				logger.debug(ignored);
			}
		} finally {
			try {
				st.close();
			} catch (SQLException e) {
			}
		}

	}

	/**
	 * Synchronizes in-memory state of an entity with the database
	 * 
	 * @param entity
	 *            the permanent entity to synchronize
	 */
	void update(PermanentEntity entity) {
		PermanentEntity pe = (PermanentEntity) entity;
		Integer id = pe.id;
		Statement s = null;

		pe.lock.lock();
		writeLock.lock();

		try {
			s = conn.createStatement();
			ResultSet rs;

			HashSet<String> identifiers = new HashSet<String>();
			HashSet<Integer> roleIds = new HashSet<Integer>();

			rs = s.executeQuery("SELECT name FROM entity_names id where id="
					+ id);
			while (rs.next()) {
				String name = rs.getString(1);

				identifiers.add(name);
			}
			rs.close();

			rs = s.executeQuery("SELECT role_id FROM entity_roles where id="
					+ id);
			while (rs.next()) {
				Integer role_id = rs.getInt(1);

				roleIds.add(role_id);
			}

			pe.identifiers = identifiers;
			pe.roleIds = roleIds;
			pe.isValid = true;
			pe.timeStamp = System.currentTimeMillis();

		} catch (SQLException e) {
			pe.isValid = false;
		} finally {
			writeLock.unlock();

			pe.lock.unlock();

			try {
				s.close();
			} catch (SQLException e) {
			}
		}
	}

}
